#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2011 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tool to supervise launch other binaries."""


import getopt
import logging
import logging.handlers
import os
import random
import signal
import subprocess
import sys
import time


class Error(Exception):
    """Base error."""


class OptionError(Error):
    """Option error."""


class TimeoutError(Error):
    """Timeout while execute() running."""


class Supervisor(object):
    def __init__(self):
        """Init."""
        self.options = {
            'timeout': None,
            'delayrandom': None,
            'stdout': None,
            'stderr': None,
            'debug': None,
        }

    def setOptions(self, **kwargs):
        for k in kwargs:
            self.options[k] = kwargs[k]

    def execute(self, args):
        """Exec.

        Args:
            args: list, arguments to execute, args[0] is binary name
        """
        logging.debug('execute(%s)' % args)
        if 'delayrandom' in self.options and self.options['delayrandom']:
          max_secs = self.options['delayrandom']
          random_secs = random.randrange(0, max_secs)
          logging.debug(
              'Applying random delay up to %s seconds: %s',
              max_secs, random_secs)
          time.sleep(random_secs)

        proc = subprocess.Popen(
            args,
            preexec_fn=lambda: os.setpgid(os.getpid(), os.getpid()),
        )

        self.returncode = None

        start_time = time.time()

        try:
            while 1:
                slept = 0

                returncode = proc.poll()
                if returncode is not None:
                    self.returncode = returncode
                    break

                if 'timeout' in self.options and self.options['timeout']:
                    if (time.time() - start_time) > self.options['timeout']:
                        raise TimeoutError

                # this loop is constructed this way, rather than using alarm or
                # something, to facilitate future features, e.g. pipe
                # stderr/stdout to syslog.
                if slept < 1:
                    time.sleep(1)
                    slept += 1

        except TimeoutError:
            logging.critical('Timeout error executing %s', ' '.join(args))
            signal.signal(signal.SIGCHLD, signal.SIG_IGN)
            os.kill(-1 * proc.pid, signal.SIGTERM)
            signal.signal(signal.SIGCHLD, signal.SIG_DFL)
            raise

    def GetReturnCode(self):
        return self.returncode


def parseOpts(argv):
    """Parse argv and return options and arguments.

    Args:
        argv: list, all argv parameters
    Returns:
        (dict of options, list extra args besides options)
    """
    try:
        argopts, args = getopt.gnu_getopt(
            argv, '',
            [
                'timeout=', 'delayrandom=', 'debug', 'help',
            ])
    except getopt.GetoptError, e:
        raise OptionError(str(e))

    options = {}
    for k, v in argopts:
        if k in ['--timeout', '--delayrandom']:
            options[k[2:]] = int(v)
        else:
            options[k[2:]] = v
    return options, args


def Usage():
    """Print usage."""
    print """supervisor [options] [--] [path to executable] [arguments]

    options:
        --timeout n
            after n seconds, terminate the executable
        --delayrandom n
            delay the execution of executable by random seconds up to n
        --debug
            enable debugging output
        --help
            this text
        --
            use the -- to separate supervisor options from arguments to the
            executable which will appear as options.
    """


def processOpts(options, args):
    """Process options for validity etc.

    Args:
        options: dict, options
        args: list, extra args
    Returns:
        True if supervisor startup should occur, False if not.
    Raises:
        OptionError: if there is an error in options
    """
    if not args or options.get('help', None) is not None:
        Usage()
        return False
    if options.get('debug', None) is not None:
        logging.getLogger().setLevel(logging.DEBUG)
    return True


def main(argv):
    try:
        options, args = parseOpts(argv[1:])
        if not processOpts(options, args):
            return 0
    except OptionError, e:
        logging.error(str(e))
        return 1

    try:
        sp = Supervisor()
        sp.setOptions(**options)
        sp.execute(args)
        return sp.GetReturnCode()
    except Error, e:
        logging.debug('%s %s', e.__class__.__name__, str(e))
        return 1


if __name__ == '__main__':
    sys.exit(main(sys.argv))
